iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

一、前言

昨天逛了 Firebase 商業街,知道了鏢局 Cloud Functions for Firebase 的規矩,
今天就來正式開張鏢局!

鏢局 Cloud Functions for Firebase ,該怎麼建置呢?

  • 要準備開發環境嗎?
  • 寫好的函式,能不能先在本機試?
  • 部署雲端之後,App 要怎麼呼叫?會不會很慢?

今天就從零開始,看看一支函式從寫好到被 App 呼叫,會經過哪些關卡!


二、開張前的準備:開發環境

函式會在本機撰寫、測試,再從本機部署到雲端,這些步驟都要靠指令列工具完成。
開始之前,先把工具準備好。

(一)要裝哪些東西?裝在哪裡?

要安裝的東西分成兩種:

類型 裝在哪裡 裝幾次 例子
工具 本機(全域安裝) 一次就好,所有專案共用 Node.js、Firebase CLI、FlutterFire CLI
套件 專案裡 每個專案各自安裝 後端的 firebase-functions、App 的 cloud_functions

這一節只安裝「工具」;套件會在建立專案時一起裝好。

工具 為什麼需要 確認指令 這次的版本
Node.js 函式用 JavaScript 撰寫,本機的模擬器、套件安裝都靠它;Firebase CLI 本身也是用 Node.js 執行 node -v 24.21.0
Firebase CLI 建立函式、啟動模擬器、部署到雲端 firebase --version 15.31.0
FlutterFire CLI 替 Flutter App 產生連到 Firebase 的設定檔 flutterfire --version 1.4.1

(二)安裝步驟

步驟 1:準備 Node.js

先確認本機的 Node.js 版本:

node -v

Firebase CLI 需要 Node.js 20 以上,
函式部署到雲端後用哪一版,則由 functions/package.json 的 engines 決定。
firebase init 產生的範本,預設就是 Node.js 24。

步驟 2:安裝 Firebase CLI

npm install -g firebase-tools
firebase --version

-g 代表全域安裝:裝一次,本機的所有專案都能使用。

步驟 3:安裝 FlutterFire CLI

dart pub global activate flutterfire_cli
flutterfire --version

如果終端機提示 ~/.pub-cache/bin 不在 PATH 裡,照提示加進去,才能直接執行 flutterfire。

(三)確認環境、常見狀況

三個確認指令都印出版本號,環境就準備好了。

狀況 原因 解法
firebase 執行時出現 Node.js 版本不符的警告或錯誤 本機的 Node.js 太舊 升級到 20 以上(建議 24)
command not found: firebase 或 flutterfire 工具的安裝位置不在 PATH 裡 重新開終端機;或依安裝時的提示設定 PATH
flutterfire 出現 Can't load Kernel binary: Invalid kernel binary format version. Dart 升級後,舊的執行檔看起來不相容了 再執行一次 dart pub global activate flutterfire_cli

三、寫第一支函式

(一)先決定函式類型:onRequest 還是 onCall

寫函式之前,要先決定App 要用什麼方式呼叫這支函式。
這個決定會影響兩件事:

  • 後端要自己處理多少事:解析送來的資料、確認呼叫的人是誰、出錯時回傳什麼。
  • App 端怎麼呼叫:自己組出 HTTP 請求,還是透過 Firebase SDK 直接呼叫。

兩種呼叫方式(函式類型),就像銀行裡不同的窗口:

  • 一般櫃台(onRequest):誰都能來辦,文件格式不限;但核對身分、檢查文件,都要櫃台人員自己來。
  • 會員專屬窗口(onCall):只收固定格式的申請單,會員的身分資料會自動帶上,辦不成也會用固定的格式回覆。

兩者的具體差異:

onRequest(HTTP 函式) onCall(Callable 函式)
誰來呼叫 任何 HTTP 用戶端(瀏覽器、curl、第三方服務) App 透過 Firebase SDK 呼叫
資料格式 自己決定:JSON、表單、檔案都可以 固定用 JSON:請求放在 data,回應放在 result
身分驗證 自己處理 SDK 自動帶上,結果放在 request.auth
出錯時 自己決定要回什麼 丟出 HttpsError,App 收到對應的錯誤代碼
適合 webhook、公開 API、給第三方呼叫 自家 App 呼叫後端

小提醒:
自家 App 要呼叫的,選 onCall 可以省下不少功夫,今天的示範專案用的就是它。

不過,會員專屬窗口只收固定格式的申請單,
也就是 callable 函式只能傳 JSON 支援的型別,沒有二進位;

如果要接 Cloud STT,就得先把錄音轉成 base64 字串,或先上傳到 Cloud Storage,
再把路徑交給函式。

(二)初始化:先用 demo- 專案

專案 ID 用 demo- 開頭,
Firebase CLI 就會把它當成示範專案,只在本機模擬器上執行。

firebase init functions --project demo-kenkou
kenkou-functions-demo
├─ .firebaserc      # 專案別名
├─ firebase.json    # 專案設定
└─ functions/
   ├─ package.json  # 相依套件與 Node.js 版本
   ├─ index.js      # 函式程式碼
   └─ .eslintrc.js  # ESLint 設定

(三)範本的兩個小狀況

1. 範本自己過不了 lint

範本裡的範例函式都是註解,但留下了沒用到的 import,還有不符合 ESLint 規則的空格。直接執行 npm run lint,會出現 4 個錯誤:

'onRequest' is assigned a value but never used
'logger' is assigned a value but never used
There should be no space after '{'
There should be no space before '}'
✖ 4 problems (4 errors, 0 warnings)

因為 firebase.json 設定了部署前先跑 lint(predeploy),範本原封不動拿去部署,會在 lint 這一步失敗。寫好自己的函式、整理掉沒用到的 import,就不會遇到這個問題。

2. ?. 語法被 ESLint 擋下

範本的 .eslintrc.js 設定 ecmaVersion: 2018,而 ?.(optional chaining)是 ES2020 才加入的語法:

Parsing error: Unexpected token .

Node.js 24 本身支援 ?.,擋下來的只是 ESLint 的檢查,把版本調高就好:

// functions/.eslintrc.js
parserOptions: {
  // ?. 是 ES2020 的語法
  "ecmaVersion": 2020,
},

(四)第一支 callable 函式:greet

收到名字,回一句早安;沒給名字,就退件:

// functions/index.js
const {setGlobalOptions} = require("firebase-functions");
const {onCall, HttpsError} = require("firebase-functions/https");
const logger = require("firebase-functions/logger");

// 所有函式預設部署到台灣,最多同時 10 個執行個體
setGlobalOptions({region: "asia-east1", maxInstances: 10});

exports.greet = onCall((request) => {
  const name = request.data?.name;
  if (typeof name !== "string" || name.length === 0) {
    // HttpsError 的 code、message 會原樣傳回 App
    throw new HttpsError("invalid-argument", "缺少 name");
  }
  logger.info("收到 greet 請求");
  return {message: `${name},早安!`};
});
寫法 說明
region: "asia-east1" 函式放在台灣;沒指定時預設是 us-central1。App 端呼叫時,也要指定同一個區域
request.data App 送來的資料
HttpsError App 會收到對應的錯誤代碼與訊息;其他沒處理的例外,App 只會收到 INTERNAL
回傳的物件 放在回應的 result 裡,送回 App

四、在本機模擬測試

(一)啟動模擬器

firebase emulators:start --project demo-kenkou
✔  functions[asia-east1-greet]: http function initialized
   (http://127.0.0.1:5001/demo-kenkou/asia-east1/greet).

模擬器的管理介面在 http://127.0.0.1:4000,看得到 log;
修改程式碼後會自動重新載入。

(二)用 curl 試打一次

callable 的協定很單純:用 POST 送出 JSON,資料放在 data 裡:

curl -H 'Content-Type: application/json' \
  -d '{"data":{"name":"阿嬤"}}' \
  http://127.0.0.1:5001/demo-kenkou/asia-east1/greet
送出 收到
{"data":{"name":"阿嬤"}} 200 {"result":{"message":"阿嬤,早安!"}}
{"data":{}} 400 {"error":{"message":"缺少 name","status":"INVALID_ARGUMENT"}}
改用 GET 呼叫 400 INVALID_ARGUMENT(callable 只收 POST)

函式丟出的 invalid-argument,
到了 HTTP 這一層,就變成 400 和 INVALID_ARGUMENT。


五、登記開業:建立專案、升級 Blaze

在後院演練沒問題,接著就要到商業街正式登記開業了。

(一)建立 Firebase 專案

欄位 這次的設定 需要留意
專案名稱 Kenkou TW Dev 之後可以修改
專案 ID kenkou-tw-dev 建立之後就不能修改;會出現在函式的網址裡
Google Analytics 關閉 這次的示範用不到

(二)升級 Blaze:設定預算提醒與支出上限

Day13 提過,部署 Cloud Functions 需要 Blaze,也介紹了防爆帳單的工具。這次設定了兩道防線:

1. 預算提醒

在 Firebase 主控台的「用量與帳單」→「帳戶和預算」,
按「查看預算」,會跳到 Google Cloud 的 Budgets & caps 建立預算:

小發現:預算頁的「$」是哪一種幣別?
Google Cloud 的預算頁只寫「$」,沒有標示幣別。

Google Cloud 帳單:
Each Cloud Billing account operates in a single currency, which you can't change after you create your Cloud Billing account.

意思是:
每個帳單帳戶只使用一種幣別,建立之後就不能更改。
上面第一張圖的「帳單帳戶幣別」就寫著 TWD,
所以預算頁上的「$100」,其實是新台幣 100 元。

2. 支出上限

Firebase 支出上限:
Cloud Functions for Firebase: Set caps on the underlying Cloud Run functions service.

意思是:
Firebase 的函式實際上跑在 Google Cloud 的 Cloud Run functions 上,
上限也是設在這裡。

費用 算在這個上限裡嗎
函式執行的費用 算
部署時用到的 Cloud Build、Artifact Registry 不算
函式裡呼叫的其他服務(例如 Cloud STT、Gemini) 不算

沒算進去的費用,要靠預算提醒盯著。

小發現:
設好之後,Google Cloud 的 Budgets & caps 清單多了一筆 Generated spend cap (Firebase Console)(下圖第二列)。
看起來 Firebase 的設定畫面只是一個捷徑,實際建立的是 Google Cloud 的預算。


六、部署上線

(一)登入並綁定專案

firebase login
firebase use --add kenkou-tw-dev --alias default

(二)部署

firebase deploy --only functions

函式部署成功了,指令卻回報錯誤

✔  functions[greet(asia-east1)] Successful create operation.
⚠  functions: No cleanup policy detected for repositories
   in asia-east1. This may result in a small monthly bill
   as container images accumulate over time.
Error: Functions successfully deployed but could not set up
cleanup policy in location asia-east1. …

函式其實已經上線了,失敗的是清理政策:
每次部署都會在 Artifact Registry 留下一份容器映像檔,沒有定期清理,
就會慢慢累積儲存費用,也就是 Day13 提過的「小額帳單」。

firebase functions:artifacts:setpolicy \
  --location asia-east1 --days 1 --force
  • --days 1:自動刪除超過 1 天的映像檔(CLI 的預設值)。
  • --location 預設是 us-central1,函式不在預設區域時,一定要指定,不然會設到錯的地方。

設好之後,再部署就不會出現這個錯誤了。

(三)從雲端呼叫函式

curl -H 'Content-Type: application/json' \
  -d '{"data":{"name":"阿嬤"}}' \
  https://asia-east1-kenkou-tw-dev.cloudfunctions.net/greet
呼叫 結果
部署後第 1 次 200,0.41 秒
第 2~6 次 200,約 0.08 秒
不帶名字 400,INVALID_ARGUMENT/缺少 name

注意:這時候,任何人只要知道網址,用 curl、Postman 就能呼叫這支函式。
網址格式是固定的,專案 ID 也打包在 App 裡,藏不起來;
如果函式呼叫的是 Gemini 這類付費服務,別人一直打,帳單可能就暴增。


七、讓 Flutter App 呼叫函式

(一)加入 Firebase:flutterfire configure

flutter pub add firebase_core cloud_functions
flutterfire configure --project=kenkou-tw-dev \
  --platforms=ios,android \
  --ios-bundle-id=tw.aarontsai.kenkouFunctionsDemo \
  --android-package-name=tw.aarontsai.kenkou_functions_demo

flutterfire configure 會替各平台在 Firebase 註冊 App,並產生設定檔:

檔案 說明
lib/firebase_options.dart 各平台的 Firebase 設定
ios/Runner/GoogleService-Info.plist iOS 的設定檔
android/app/google-services.json Android 的設定檔
android/settings.gradle.kts 等 加上 Google Services 的 Gradle 外掛

App 啟動時初始化 Firebase:

await Firebase.initializeApp(
  options: DefaultFirebaseOptions.currentPlatform,
);

(二)呼叫函式

// 函式在 asia-east1,App 端也要指定同一個區域
final functions =
    FirebaseFunctions.instanceFor(region: 'asia-east1');

try {
  final result = await functions
      .httpsCallable('greet')
      .call({'name': '阿嬤'});
  print(result.data['message']);
} on FirebaseFunctionsException catch (e) {
  // e.code 對應後端 HttpsError 的代碼
  print('${e.code}: ${e.message}');
}

App 端指定的區域要跟函式一致:
用 FirebaseFunctions.instance 的話,預設會去 us-central1 找函式。

執行後,App 收到「阿嬤,早安!」,就代表 App 成功呼叫到雲端的函式了。


八、小結

今天鏢局正式開張,從零走到 App 呼叫成功:

  • 選類型:自家 App 呼叫,可以優先考慮 onCall;callable 只收 JSON。
  • 寫函式:demo- 專案不用建專案、不用帳單,就能先寫、先測;範本有 lint 和 ecmaVersion 兩個小坑。
  • 登記開業:專案 ID 建立後不能改;預算提醒與支出上限都設好了,預算頁的「$」是帳單帳戶的幣別。
  • 部署:記得替函式所在的區域設清理政策。
  • App 呼叫:flutterfire configure 產生設定,區域要跟函式一致

工具備齊、本機演練過關、雲端正式開張:
App 託付的第一趟鏢,順利送達,也平安帶了回來!


九、預告

開張第一天,也發現了一件讓人不太放心的事:
任何人只要知道網址,都能直接呼叫這支函式,不一定是自家的 App。

所以,
鏢局需要增添守衛,確認上門的真的是自家的 App;
鏢局裡值錢的金鑰,也需要一個保險箱。

明天就來替鏢局請守衛、打造保險箱:Cloud Functions for Firebase(下)!

感謝有緣看到這邊的你~
希望佛菩薩也祝福你:🌟平安開心 幸福順遂🌟
南無觀世音菩薩🍀 南無地藏菩薩🏠 南無阿彌陀佛☀️


上一篇
Cloud Functions for Firebase(上)
下一篇
Cloud Functions for Firebase(下)
系列文
Build on Google AI :長者照護 —— 口腔機能訓練 與 延緩認知退化 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言